<template>
  <div v-show="dragging" class="grid-view__row-dragging-container">
    <div
      class="grid-view__row-dragging"
      :style="{ width: width + 'px', top: draggingTop + 'px' }"
    ></div>
    <div
      class="grid-view__row-target"
      :style="{ width: width + 'px', top: targetTop + 'px' }"
    ></div>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'

import { notifyIf } from '@baserow/modules/core/utils/error'
import gridViewHelpers from '@baserow/modules/database/mixins/gridViewHelpers'

export default {
  name: 'GridViewRowDragging',
  mixins: [gridViewHelpers],
  props: {
    table: {
      type: Object,
      required: true,
    },
    view: {
      type: Object,
      required: true,
    },
    primary: {
      type: Object,
      required: true,
    },
    fields: {
      type: Array,
      required: true,
    },
    vertical: {
      type: String,
      required: true,
    },
  },
  data() {
    return {
      // Indicates if the user is dragging a row to another position.
      dragging: false,
      // The row object that is being dragged.
      row: null,
      // The top position of the row.
      rowTop: 0,
      // The row where the dragged row must be placed before.
      targetRow: null,
      // The horizontal starting position of the mouse.
      mouseStart: 0,
      // The position of the dragging animation.
      draggingTop: 0,
      // The position of the target indicator where the field is going to be moved to.
      targetTop: 0,
      // The mouse move event.
      lastMoveEvent: null,
      // Indicates if the user is auto scrolling at the moment.
      autoScrolling: false,
    }
  },
  computed: {
    width() {
      const allFields = [this.primary].concat(this.fields)
      return (
        allFields.reduce(
          (value, field) => this.getFieldWidth(field.id) + value,
          0
        ) + this.gridViewRowDetailsWidth
      )
    },
  },
  beforeCreate() {
    this.$options.computed = {
      ...(this.$options.computed || {}),
      ...mapGetters({
        rowHeight:
          this.$options.propsData.storePrefix + 'view/grid/getRowHeight',
        bufferStartIndex:
          this.$options.propsData.storePrefix + 'view/grid/getBufferStartIndex',
        rowsCount: this.$options.propsData.storePrefix + 'view/grid/getCount',
        allRows: this.$options.propsData.storePrefix + 'view/grid/getAllRows',
      }),
    }
  },
  beforeDestroy() {
    this.cancel()
  },
  methods: {
    getRowTop(rowId) {
      const index = this.allRows.findIndex((row) => row.id === rowId)
      if (index < 0) {
        return 0
      }

      return this.bufferStartIndex * this.rowHeight + index * this.rowHeight
    },
    /**
     * Called when the row dragging must start. It will register the global mouse
     * move, mouse up events and keyup events so that the user can drag the field to
     * the correct position.
     */
    start(row, event) {
      const element = this.$parent[this.vertical]()

      this.row = row
      this.startRowTop = this.getRowTop(row.id) - element.scrollTop
      this.targetRowId = row.id
      this.dragging = true
      this.mouseStart = event.clientY
      this.draggingTop = 0
      this.targetTop = 0

      this.$el.moveEvent = (event) => this.move(event)
      window.addEventListener('mousemove', this.$el.moveEvent)

      this.$el.upEvent = (event) => this.up(event)
      window.addEventListener('mouseup', this.$el.upEvent)

      this.$el.keydownEvent = (event) => {
        if (event.keyCode === 27) {
          // When the user presses the escape key we want to cancel the action
          this.cancel(event)
        }
      }
      document.body.addEventListener('keydown', this.$el.keydownEvent)
      this.move(event, false)
    },
    /**
     * The move method is called when every time the user moves the mouse while
     * dragging a row. It can also be called while auto scrolling.
     */
    move(event = null, startAutoScroll = true) {
      if (event !== null) {
        event.preventDefault()
        this.lastMoveEvent = event
      } else {
        event = this.lastMoveEvent
      }

      // This is the vertically scrollable element.
      const element = this.$parent[this.vertical]()
      const elementRect = element.getBoundingClientRect()
      const elementHeight = elementRect.bottom - elementRect.top

      // Calculate the top position of the dragging effect. Note that this effect lays
      // over the vertically scrollable rows.
      this.draggingTop = Math.max(
        0,
        Math.min(
          this.startRowTop + event.clientY - this.mouseStart,
          elementHeight - this.rowHeight
        )
      )

      // Calculate before which row we want to place the row that is currently being
      // dragged. We also calculate target top position which indicates at which
      // position the row is going to be placed. Note that the target effect lays over
      // the vertically scrollable rows.
      const mouseTop = event.clientY - elementRect.top + element.scrollTop
      const rowIndex = Math.max(
        0,
        Math.min(Math.round(mouseTop / this.rowHeight), this.rowsCount)
      )
      this.targetTop = rowIndex * this.rowHeight - element.scrollTop
      const beforeRow = this.allRows[rowIndex - this.bufferStartIndex]
      this.targetRow = beforeRow === undefined ? null : beforeRow

      // If the user is not already auto scrolling, which happens while dragging and
      // moving the element close to the end of the view port at the top or bottom
      // side, we might need to initiate that process.
      if (!this.autoScrolling || !startAutoScroll) {
        const side = Math.ceil((elementHeight / 100) * 10)
        const autoScrollMouseTop = event.clientY - elementRect.top
        const autoScrollMouseBottom = elementHeight - autoScrollMouseTop
        let speed = 0

        if (autoScrollMouseTop < side) {
          speed = -(6 - Math.ceil((Math.max(0, autoScrollMouseTop) / side) * 6))
        } else if (autoScrollMouseBottom < side) {
          speed = 6 - Math.ceil((Math.max(0, autoScrollMouseBottom) / side) * 6)
        }

        // If the speed is either a position or negative, so not 0, we know that we
        // need to start auto scrolling.
        if (speed !== 0) {
          this.autoScrolling = true
          this.$emit('scroll', { pixelY: speed, pixelX: 0 })
          this.$el.scrollTimeout = setTimeout(() => {
            this.move(null, false)
          }, 1)
        } else {
          this.autoScrolling = false
        }
      }
    },
    /**
     * Can be called when the current dragging state needs to be stopped. It will
     * remove all the created event listeners and timeouts.
     */
    cancel() {
      this.dragging = false
      window.removeEventListener('mousemove', this.$el.moveEvent)
      window.removeEventListener('mouseup', this.$el.upEvent)
      document.body.addEventListener('keydown', this.$el.keydownEvent)
      clearTimeout(this.$el.scrollTimeout)
    },
    /**
     * Called when the user releases the mouse on a the desired position. It will
     * calculate the new position of the row in the list and if it has changed
     * position, then the order in the row options is updated accordingly.
     */
    async up(event) {
      event.preventDefault()
      this.cancel()

      // We don't need to do anything if the row must be placed before or after itself
      // because that wouldn't change the position.
      if (this.targetRow !== null) {
        // If the row must be placed before itself.
        if (this.row.id === this.targetRow.id) {
          return
        }

        // If the row needs to be placed after itself.
        const allRows =
          this.$store.getters[this.storePrefix + 'view/grid/getAllRows']
        const index = allRows.findIndex((r) => r.id === this.targetRow.id)
        const after = allRows[index - 1]
        if (after && this.row.id === after.id) {
          return
        }
      }

      const element = this.$parent[this.vertical]()
      const getScrollTop = () => element.scrollTop

      try {
        await this.$store.dispatch(this.storePrefix + 'view/grid/moveRow', {
          table: this.table,
          grid: this.view,
          fields: this.fields,
          primary: this.primary,
          getScrollTop,
          row: this.row,
          before: this.targetRow,
        })
      } catch (error) {
        notifyIf(error, 'row')
      }
    },
  },
}
</script>
